{
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "In this tutorial, we will walk through how to build an agent that can answer questions about a SQL database.\n",
    "\n",
    "At a high level, the agent will: 1. Fetch the available tables from the database 2. Decide which tables are relevant to the question 3. Fetch the DDL for the relevant tables 4. Generate a query based on the question and information from the DDL 5. Double-check the query for common mistakes using an LLM 6. Execute the query and return the results 7. Correct mistakes surfaced by the database engine until the query is successful 8. Formulate a response based on the results\n",
    "\n",
    "https://langchain-ai.github.io/langgraph/tutorials/sql-agent/\n",
    "\n",
    "## 配置数据库"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "524e5b7a1a131d2a"
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sqlite\n",
      "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n"
     ]
    },
    {
     "data": {
      "text/plain": "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\""
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_community.utilities import SQLDatabase\n",
    "\n",
    "db = SQLDatabase.from_uri(\"sqlite:///../../lang_chain/cookbook/Chinook.db\")\n",
    "print(db.dialect)\n",
    "print(db.get_usable_table_names())\n",
    "db.run(\"SELECT * FROM Artist LIMIT 10;\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:18:55.750179Z",
     "start_time": "2024-11-29T01:18:53.339043Z"
    }
   },
   "id": "d95b0fbd0acbf648",
   "execution_count": 2
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Utility functions\n",
    "我们将定义一个实用函数来帮助我们实现代理。具体来说，我们将使用回退来包装ToolNode，以处理错误并将它们呈现给代理。\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "9b40c73da00b72a5"
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "\n",
    "from langchain_core.messages import ToolMessage\n",
    "from langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\n",
    "from langgraph.prebuilt import ToolNode\n",
    "\n",
    "\n",
    "def create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n",
    "    \"\"\"\n",
    "    创建一个具有回退功能的ToolNode来处理错误并将其显示给代理。\n",
    "    \"\"\"\n",
    "    return ToolNode(tools).with_fallbacks(\n",
    "        [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n",
    "    )\n",
    "\n",
    "\n",
    "def handle_tool_error(state) -> dict:\n",
    "    error = state.get(\"error\")\n",
    "    tool_calls = state[\"messages\"][-1].tool_calls\n",
    "    return {\n",
    "        \"messages\": [\n",
    "            ToolMessage(\n",
    "                content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n",
    "                tool_call_id=tc[\"id\"],\n",
    "            )\n",
    "            for tc in tool_calls\n",
    "        ]\n",
    "    }\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:18:56.892296Z",
     "start_time": "2024-11-29T01:18:55.752028Z"
    }
   },
   "id": "ba832b7ef2e01f39",
   "execution_count": 3
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Define tools for the agent\n",
    "\n",
    "- list_tables_tool: 列出数据库中的可用的表(使用SQLDatabaseToolkit)\n",
    "- get_table_schema_tool: 获取指定表的DDL(使用SQLDatabaseToolkit)\n",
    "- db_query_tool: 执行查询并获取结果，如果查询失败，则返回错误消息"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "76e5ffa4fdbe0894"
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "\n",
      "CREATE TABLE \"Artist\" (\n",
      "\t\"ArtistId\" INTEGER NOT NULL, \n",
      "\t\"Name\" NVARCHAR(120), \n",
      "\tPRIMARY KEY (\"ArtistId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Artist table:\n",
      "ArtistId\tName\n",
      "1\tAC/DC\n",
      "2\tAccept\n",
      "3\tAerosmith\n",
      "*/\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "from dotenv import load_dotenv\n",
    "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "load_dotenv()\n",
    "toolkit = SQLDatabaseToolkit(db=db, llm=ChatOpenAI(\n",
    "    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "    openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "    model_name=\"qwen-max\",\n",
    "    verbose=True, temperature=0\n",
    "))\n",
    "tools = toolkit.get_tools()\n",
    "\n",
    "list_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\n",
    "get_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n",
    "sql_db_query = next(tool for tool in tools if tool.name == \"sql_db_query\")\n",
    "\n",
    "print(list_tables_tool.invoke(\"\"))\n",
    "\n",
    "print(get_schema_tool.invoke(\"Artist\"))"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:18:58.005755Z",
     "start_time": "2024-11-29T01:18:56.894126Z"
    }
   },
   "id": "4f429fddb835f3fb",
   "execution_count": 4
  },
  {
   "cell_type": "markdown",
   "source": [
    "The third will be defined manually. For the db_query_tool, we will execute the query against the database and return the results.\n",
    "\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "2d296fac6b416bdb"
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "from langchain_core.tools import tool\n",
    "\n",
    "\n",
    "# @tool\n",
    "# def db_query_tool(query: str) -> str:\n",
    "#     \"\"\"\n",
    "#     Execute a SQL query against the database and get back the result.\n",
    "#     If the query is not correct, an error message will be returned.\n",
    "#     If an error is returned, rewrite the query, check the query, and try again.\n",
    "#     \"\"\"\n",
    "#     result = db.run_no_throw(query)\n",
    "#     if not result:\n",
    "#         return \"Error: Query failed. Please rewrite your query and try again.\"\n",
    "#     return result\n",
    "# \n",
    "# \n",
    "# print(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:18:58.019466Z",
     "start_time": "2024-11-29T01:18:58.012412Z"
    }
   },
   "id": "2f863003c2f1bb93",
   "execution_count": 5
  },
  {
   "cell_type": "markdown",
   "source": [
    "While not strictly a tool, we will prompt an LLM to check for common mistakes in the query and later add this as a node in the workflow.\n",
    "\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "eb5bd95fe7c1ec06"
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "data": {
      "text/plain": "AIMessage(content=\"The provided query appears to be simple and correct. There are no issues with the points you've mentioned, such as `NOT IN` with `NULL` values, `UNION` vs `UNION ALL`, or any of the other common mistakes. The query selects all columns from the `Artist` table and limits the result to 10 rows.\\n\\nI will now call the appropriate tool to execute the query.\", additional_kwargs={'tool_calls': [{'id': 'call_af8283496aab4c30a8092a', 'function': {'arguments': '{\"query\": \"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'sql_db_query'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 110, 'prompt_tokens': 377, 'total_tokens': 487, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen-max', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-237b0a5d-ed93-4fd1-801d-fa60d4cc5ff2-0', tool_calls=[{'name': 'sql_db_query', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_af8283496aab4c30a8092a', 'type': 'tool_call'}], usage_metadata={'input_tokens': 377, 'output_tokens': 110, 'total_tokens': 487, 'input_token_details': {}, 'output_token_details': {}})"
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "query_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
    "Double check the SQLite query for common mistakes, including:\n",
    "- Using NOT IN with NULL values\n",
    "- Using UNION when UNION ALL should have been used\n",
    "- Using BETWEEN for exclusive ranges\n",
    "- Data type mismatch in predicates\n",
    "- Properly quoting identifiers\n",
    "- Using the correct number of arguments for functions\n",
    "- Casting to the correct data type\n",
    "- Using the proper columns for joins\n",
    "\n",
    "If there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n",
    "\n",
    "You will call the appropriate tool to execute the query after running this check.\"\"\"\n",
    "\n",
    "query_check_prompt = ChatPromptTemplate.from_messages(\n",
    "    [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n",
    ")\n",
    "check_llm = ChatOpenAI(\n",
    "    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "    openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "    model_name=\"qwen-max\",\n",
    "    verbose=True, temperature=0\n",
    ")\n",
    "query_check = query_check_prompt | check_llm.bind_tools([sql_db_query])\n",
    "\n",
    "# from langchain_community.tools import QuerySQLCheckerTool\n",
    "# \n",
    "# sql_check = QuerySQLCheckerTool(db=db, llm=check_llm)\n",
    "\n",
    "query_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:19:04.670208Z",
     "start_time": "2024-11-29T01:18:58.021096Z"
    }
   },
   "id": "7f1e7825372b88fb",
   "execution_count": 6
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Define the workflow\n",
    "我们将为代理定义工作流。代理将首先强制调用list_tables_tool从数据库中获取可用的表，然后按照本教程开头提到的步骤进行操作。"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "f471342d0a8a9d3"
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "from langchain_community.chat_models import ChatTongyi\n",
    "from typing import Annotated, Literal\n",
    "\n",
    "from langchain_core.messages import AIMessage\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "from pydantic import BaseModel, Field\n",
    "from typing_extensions import TypedDict\n",
    "\n",
    "from langgraph.graph import END, StateGraph, START\n",
    "from langgraph.graph.message import AnyMessage, add_messages\n",
    "\n",
    "\n",
    "# Define the state for the agent\n",
    "class State(TypedDict):\n",
    "    messages: Annotated[list[AnyMessage], add_messages]\n",
    "\n",
    "\n",
    "# Define a new graph\n",
    "workflow = StateGraph(State)\n",
    "\n",
    "\n",
    "# Add a node for the first tool call\n",
    "def first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n",
    "    return {\n",
    "        \"messages\": [\n",
    "            AIMessage(\n",
    "                content=\"\",\n",
    "                tool_calls=[\n",
    "                    {\n",
    "                        \"name\": \"sql_db_list_tables\",\n",
    "                        \"args\": {},\n",
    "                        \"id\": \"tool_abcd123\",\n",
    "                    }\n",
    "                ],\n",
    "            )\n",
    "        ]\n",
    "    }\n",
    "\n",
    "\n",
    "def model_check_query(state: State) -> dict[str, list[AIMessage]]:\n",
    "    \"\"\"\n",
    "    Use this tool to double-check if your query is correct before executing it.\n",
    "    \"\"\"\n",
    "    return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n",
    "\n",
    "\n",
    "workflow.add_node(\"first_tool_call\", first_tool_call)\n",
    "\n",
    "# Add nodes for the first two tools\n",
    "workflow.add_node(\n",
    "    \"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool])\n",
    ")\n",
    "workflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n",
    "\n",
    "# Add a node for a model to choose the relevant tables based on the question and available tables\n",
    "model_get_schema = ChatOpenAI(\n",
    "    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "    openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "    model_name=\"qwen-max\",\n",
    "    verbose=True, temperature=0\n",
    ").bind_tools(\n",
    "    [get_schema_tool]\n",
    ")\n",
    "workflow.add_node(\n",
    "    \"model_get_schema\",\n",
    "    lambda state: {\n",
    "        \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n",
    "    },\n",
    ")\n",
    "\n",
    "\n",
    "# Describe a tool to represent the end state\n",
    "class SubmitFinalAnswer(BaseModel):\n",
    "    \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n",
    "\n",
    "    final_answer: str = Field(..., description=\"The final answer to the user\")\n",
    "\n",
    "\n",
    "# Add a node for a model to generate a query based on the question and schema\n",
    "query_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
    "\n",
    "Given an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n",
    "\n",
    "DO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n",
    "\n",
    "When generating the query:\n",
    "\n",
    "Output the SQL query that answers the input question without a tool call.\n",
    "\n",
    "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\n",
    "You can order the results by a relevant column to return the most interesting examples in the database.\n",
    "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n",
    "\n",
    "If you get an error while executing a query, rewrite the query and try again.\n",
    "\n",
    "If you get an empty result set, you should try to rewrite the query to get a non-empty result set. \n",
    "NEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n",
    "\n",
    "If you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n",
    "\n",
    "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\n",
    "query_gen_prompt = ChatPromptTemplate.from_messages(\n",
    "    [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n",
    ")\n",
    "query_gen = query_gen_prompt | ChatOpenAI(\n",
    "    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "    openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "    model_name=\"qwen-max\",\n",
    "    verbose=True, temperature=0\n",
    ").bind_tools(\n",
    "    [sql_db_query]\n",
    ")\n",
    "\n",
    "\n",
    "# query_gen = query_gen_prompt | ChatTongyi(\n",
    "#         # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "#         api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "#         # openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "#         model=\"qwen-max\",\n",
    "#         top_p=0.9,\n",
    "#         verbose=True, temperature=0.5\n",
    "#     ).bind_tools(\n",
    "#     [sql_db_query]\n",
    "# )\n",
    "\n",
    "\n",
    "def query_gen_node(state: State):\n",
    "    message = query_gen.invoke(state)\n",
    "\n",
    "    # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n",
    "    tool_messages = []\n",
    "    # if message.tool_calls:\n",
    "    #     for tc in message.tool_calls:\n",
    "    #         if tc[\"name\"] != \"SubmitFinalAnswer\":\n",
    "    #             tool_messages.append(\n",
    "    #                 ToolMessage(\n",
    "    #                     content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",\n",
    "    #                     tool_call_id=tc[\"id\"],\n",
    "    #                 )\n",
    "    #             )\n",
    "    # else:\n",
    "    #     tool_messages = []\n",
    "    return {\"messages\": [message] + tool_messages}\n",
    "\n",
    "\n",
    "workflow.add_node(\"query_gen\", query_gen_node)\n",
    "\n",
    "# Add a node for the model to check the query before executing it\n",
    "# workflow.add_node(\"correct_query\", model_check_query)\n",
    "\n",
    "# Add node for executing the query\n",
    "workflow.add_node(\"execute_query\", create_tool_node_with_fallback([sql_db_query]))\n",
    "\n",
    "\n",
    "# Define a conditional edge to decide whether to continue or end the workflow\n",
    "def should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n",
    "    messages = state[\"messages\"]\n",
    "    last_message = messages[-1]\n",
    "    # If there is a tool call, then we finish\n",
    "    if getattr(last_message, \"tool_calls\", None):\n",
    "        return END\n",
    "    if last_message.content.startswith(\"Error:\"):\n",
    "        return \"query_gen\"\n",
    "    else:\n",
    "        return \"correct_query\"\n",
    "\n",
    "\n",
    "# Specify the edges between the nodes\n",
    "workflow.add_edge(START, \"first_tool_call\")\n",
    "workflow.add_edge(\"first_tool_call\", \"list_tables_tool\")\n",
    "workflow.add_edge(\"list_tables_tool\", \"model_get_schema\")\n",
    "workflow.add_edge(\"model_get_schema\", \"get_schema_tool\")\n",
    "workflow.add_edge(\"get_schema_tool\", \"query_gen\")\n",
    "workflow.add_edge(\"query_gen\", \"execute_query\")\n",
    "# workflow.add_conditional_edges(\n",
    "#     \"query_gen\",\n",
    "#     should_continue,\n",
    "# )\n",
    "# workflow.add_edge(\"correct_query\", \"execute_query\")\n",
    "# workflow.add_edge(\"execute_query\", \"query_gen\")\n",
    "\n",
    "# Compile the workflow into a runnable\n",
    "app = workflow.compile()"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:19:04.904961Z",
     "start_time": "2024-11-29T01:19:04.673274Z"
    }
   },
   "id": "90e51b4efff04583",
   "execution_count": 7
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": "<IPython.core.display.Image object>"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "from langchain_core.runnables.graph import MermaidDrawMethod\n",
    "\n",
    "display(\n",
    "    Image(\n",
    "        app.get_graph().draw_mermaid_png(\n",
    "            draw_method=MermaidDrawMethod.API,\n",
    "        )\n",
    "    )\n",
    ")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:19:06.014878Z",
     "start_time": "2024-11-29T01:19:04.906889Z"
    }
   },
   "id": "36827c1543d8e45b",
   "execution_count": 8
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Run the agent\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "37cf21f2a9170658"
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "# messages = app.invoke(\n",
    "#     {\"messages\": [(\"user\", \"我需要10人生日是1970年之前的员工\")]}\n",
    "# )\n",
    "# json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"][\"final_answer\"]\n",
    "# json_str"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:19:06.019905Z",
     "start_time": "2024-11-29T01:19:06.017066Z"
    }
   },
   "id": "e213d317b4c583f9",
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "event {'first_tool_call': {'messages': [AIMessage(content='', additional_kwargs={}, response_metadata={}, id='584f2d14-22f4-42b0-a9c2-2178cf8684a0', tool_calls=[{'name': 'sql_db_list_tables', 'args': {}, 'id': 'tool_abcd123', 'type': 'tool_call'}])]}}\n",
      "Assistant: \n",
      "event {'list_tables_tool': {'messages': [ToolMessage(content='Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track', name='sql_db_list_tables', id='0663c57f-9f9d-4bbe-93cc-7136e4b1182c', tool_call_id='tool_abcd123')]}}\n",
      "Assistant: Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "event {'model_get_schema': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_18f1bd851cd94872a92b71', 'function': {'arguments': '{\"table_names\": \"Employee\"}', 'name': 'sql_db_schema'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 297, 'total_tokens': 318, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen-max', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-9a9b39f9-94f6-4cf2-a1a7-3b9646534943-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Employee'}, 'id': 'call_18f1bd851cd94872a92b71', 'type': 'tool_call'}], usage_metadata={'input_tokens': 297, 'output_tokens': 21, 'total_tokens': 318, 'input_token_details': {}, 'output_token_details': {}})]}}\n",
      "Assistant: \n",
      "event {'get_schema_tool': {'messages': [ToolMessage(content='\\nCREATE TABLE \"Employee\" (\\n\\t\"EmployeeId\" INTEGER NOT NULL, \\n\\t\"LastName\" NVARCHAR(20) NOT NULL, \\n\\t\"FirstName\" NVARCHAR(20) NOT NULL, \\n\\t\"Title\" NVARCHAR(30), \\n\\t\"ReportsTo\" INTEGER, \\n\\t\"BirthDate\" DATETIME, \\n\\t\"HireDate\" DATETIME, \\n\\t\"Address\" NVARCHAR(70), \\n\\t\"City\" NVARCHAR(40), \\n\\t\"State\" NVARCHAR(40), \\n\\t\"Country\" NVARCHAR(40), \\n\\t\"PostalCode\" NVARCHAR(10), \\n\\t\"Phone\" NVARCHAR(24), \\n\\t\"Fax\" NVARCHAR(24), \\n\\t\"Email\" NVARCHAR(60), \\n\\tPRIMARY KEY (\"EmployeeId\"), \\n\\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\\n)\\n\\n/*\\n3 rows from Employee table:\\nEmployeeId\\tLastName\\tFirstName\\tTitle\\tReportsTo\\tBirthDate\\tHireDate\\tAddress\\tCity\\tState\\tCountry\\tPostalCode\\tPhone\\tFax\\tEmail\\n1\\tAdams\\tAndrew\\tGeneral Manager\\tNone\\t1962-02-18 00:00:00\\t2002-08-14 00:00:00\\t11120 Jasper Ave NW\\tEdmonton\\tAB\\tCanada\\tT5K 2N1\\t+1 (780) 428-9482\\t+1 (780) 428-3457\\tandrew@chinookcorp.com\\n2\\tEdwards\\tNancy\\tSales Manager\\t1\\t1958-12-08 00:00:00\\t2002-05-01 00:00:00\\t825 8 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 2T3\\t+1 (403) 262-3443\\t+1 (403) 262-3322\\tnancy@chinookcorp.com\\n3\\tPeacock\\tJane\\tSales Support Agent\\t2\\t1973-08-29 00:00:00\\t2002-04-01 00:00:00\\t1111 6 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 5M5\\t+1 (403) 262-3443\\t+1 (403) 262-6712\\tjane@chinookcorp.com\\n*/', name='sql_db_schema', id='ced33075-bbe8-477a-87f0-e0222ecdb5fe', tool_call_id='call_18f1bd851cd94872a92b71')]}}\n",
      "Assistant: \n",
      "CREATE TABLE \"Employee\" (\n",
      "\t\"EmployeeId\" INTEGER NOT NULL, \n",
      "\t\"LastName\" NVARCHAR(20) NOT NULL, \n",
      "\t\"FirstName\" NVARCHAR(20) NOT NULL, \n",
      "\t\"Title\" NVARCHAR(30), \n",
      "\t\"ReportsTo\" INTEGER, \n",
      "\t\"BirthDate\" DATETIME, \n",
      "\t\"HireDate\" DATETIME, \n",
      "\t\"Address\" NVARCHAR(70), \n",
      "\t\"City\" NVARCHAR(40), \n",
      "\t\"State\" NVARCHAR(40), \n",
      "\t\"Country\" NVARCHAR(40), \n",
      "\t\"PostalCode\" NVARCHAR(10), \n",
      "\t\"Phone\" NVARCHAR(24), \n",
      "\t\"Fax\" NVARCHAR(24), \n",
      "\t\"Email\" NVARCHAR(60), \n",
      "\tPRIMARY KEY (\"EmployeeId\"), \n",
      "\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Employee table:\n",
      "EmployeeId\tLastName\tFirstName\tTitle\tReportsTo\tBirthDate\tHireDate\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\n",
      "1\tAdams\tAndrew\tGeneral Manager\tNone\t1962-02-18 00:00:00\t2002-08-14 00:00:00\t11120 Jasper Ave NW\tEdmonton\tAB\tCanada\tT5K 2N1\t+1 (780) 428-9482\t+1 (780) 428-3457\tandrew@chinookcorp.com\n",
      "2\tEdwards\tNancy\tSales Manager\t1\t1958-12-08 00:00:00\t2002-05-01 00:00:00\t825 8 Ave SW\tCalgary\tAB\tCanada\tT2P 2T3\t+1 (403) 262-3443\t+1 (403) 262-3322\tnancy@chinookcorp.com\n",
      "3\tPeacock\tJane\tSales Support Agent\t2\t1973-08-29 00:00:00\t2002-04-01 00:00:00\t1111 6 Ave SW\tCalgary\tAB\tCanada\tT2P 5M5\t+1 (403) 262-3443\t+1 (403) 262-6712\tjane@chinookcorp.com\n",
      "*/\n",
      "event {'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_1053aece3db64aa3a28a1d', 'function': {'arguments': '{\"query\": \"SELECT EmployeeId, FirstName, LastName, BirthDate FROM Employee WHERE BirthDate < \\'1970-01-01\\' LIMIT 5;\"}', 'name': 'sql_db_query'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 51, 'prompt_tokens': 1164, 'total_tokens': 1215, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen-max', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-53cf2069-34d9-461c-91e9-5a7f0981189a-0', tool_calls=[{'name': 'sql_db_query', 'args': {'query': \"SELECT EmployeeId, FirstName, LastName, BirthDate FROM Employee WHERE BirthDate < '1970-01-01' LIMIT 5;\"}, 'id': 'call_1053aece3db64aa3a28a1d', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1164, 'output_tokens': 51, 'total_tokens': 1215, 'input_token_details': {}, 'output_token_details': {}})]}}\n",
      "Assistant: \n",
      "event {'execute_query': {'messages': [ToolMessage(content=\"[(1, 'Andrew', 'Adams', '1962-02-18 00:00:00'), (2, 'Nancy', 'Edwards', '1958-12-08 00:00:00'), (4, 'Margaret', 'Park', '1947-09-19 00:00:00'), (5, 'Steve', 'Johnson', '1965-03-03 00:00:00'), (8, 'Laura', 'Callahan', '1968-01-09 00:00:00')]\", name='sql_db_query', id='313fe8d1-4749-402c-ba7e-429fa9288c7e', tool_call_id='call_1053aece3db64aa3a28a1d')]}}\n",
      "Assistant: [(1, 'Andrew', 'Adams', '1962-02-18 00:00:00'), (2, 'Nancy', 'Edwards', '1958-12-08 00:00:00'), (4, 'Margaret', 'Park', '1947-09-19 00:00:00'), (5, 'Steve', 'Johnson', '1965-03-03 00:00:00'), (8, 'Laura', 'Callahan', '1968-01-09 00:00:00')]\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.messages import BaseMessage\n",
    "\n",
    "for event in app.stream(\n",
    "        {\"messages\": [(\"user\", \"我需要1人生日是1970年之前的员工\")]}\n",
    "):\n",
    "    print(f\"event {event}\")\n",
    "    for value in event.values():\n",
    "        if isinstance(value[\"messages\"][-1], BaseMessage):\n",
    "            print(\"Assistant:\", value[\"messages\"][-1].content)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:19:10.150703Z",
     "start_time": "2024-11-29T01:19:06.022335Z"
    }
   },
   "id": "eb02e8dcb8de03ce",
   "execution_count": 10
  },
  {
   "cell_type": "markdown",
   "source": [
    "# 使用lang-chain自带的toolkit"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "88fa8f599e2dfb30"
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'UNIQUE KEY(`id`, `enterprise_id`)'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'COMMENT \"OLAP\"'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'DISTRIBUTED BY HASH(`id`) BUCKETS 6'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'PROPERTIES ('\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: '\"replication_allocation\" = \"tag.location.default: 1\",'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: '\"in_memory\" = \"false\",'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: '\"storage_format\" = \"V2\"'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'UNIQUE KEY(`id`, `tel`, `cmpp_id`)'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'DISTRIBUTED BY HASH(`cmpp_id`) BUCKETS 10'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: \"  INDEX index_ymd (`ymd`) USING BITMAP COMMENT 'ymd索引',\"\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: \"  INDEX index_tel (`tel`) USING BITMAP COMMENT 'tel索引',\"\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: \"  INDEX index_task_id (`task_id`) USING BITMAP COMMENT '任务id索引'\"\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'UNIQUE KEY(`task_submit_time`, `enterprise_id`, `task_id`, `id`)'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: 'DISTRIBUTED BY HASH(`enterprise_id`) BUCKETS 10'\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: \"  INDEX index_id (`id`) USING BITMAP COMMENT 'id索引'\"\n",
      "  self._metadata.reflect(\n",
      "/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Unknown schema content: '\"bloom_filter_columns\" = \"tel\",'\n",
      "  self._metadata.reflect(\n"
     ]
    }
   ],
   "source": [
    "db_url = \"mysql+pymysql://jf:jf#5402@10.8.0.1:6033/jf\"\n",
    "table_name = 'sms_submit_stat_record,sms_arrive_stat_record,sms_upstream_details,dwd_sms_task_tag'\n",
    "\n",
    "db = SQLDatabase.from_uri(db_url, include_tables=table_name.split(','))\n",
    "load_dotenv()\n",
    "llm = ChatOpenAI(\n",
    "    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "    openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "    model_name=\"qwen-max\",\n",
    "    verbose=True, temperature=0,top_p=0.9\n",
    ")\n",
    "toolkit = SQLDatabaseToolkit(db=db, llm=llm)\n",
    "\n",
    "context = toolkit.get_context()\n",
    "tools = toolkit.get_tools()"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T03:17:34.674311Z",
     "start_time": "2024-11-29T03:17:32.588996Z"
    }
   },
   "id": "2ef03f77f97d3f60",
   "execution_count": 74
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "data": {
      "text/plain": "<langgraph.graph.state.StateGraph at 0x1224097c0>"
     },
     "execution_count": 67,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from typing import Annotated\n",
    "from langchain_openai import ChatOpenAI\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import StateGraph\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "class State(TypedDict):\n",
    "    messages: Annotated[list, add_messages]\n",
    "\n",
    "graph_builder = StateGraph(State)  # expt_llm = \"gpt-4-1106-preview\"\n",
    "\n",
    "# Modification: tell the LLM which tools it can call\n",
    "llm_with_tools = llm.bind_tools(tools)\n",
    "\n",
    "\n",
    "def chatbot(state: State):\n",
    "    return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
    "\n",
    "\n",
    "graph_builder.add_node(\"chatbot\", chatbot)\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T03:16:03.450117Z",
     "start_time": "2024-11-29T03:16:03.433766Z"
    }
   },
   "id": "cff4a5f3e176649a",
   "execution_count": 67
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "ename": "ValueError",
     "evalue": "Node `tools` already present.",
     "output_type": "error",
     "traceback": [
      "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
      "\u001B[0;31mValueError\u001B[0m                                Traceback (most recent call last)",
      "Cell \u001B[0;32mIn[69], line 27\u001B[0m\n\u001B[1;32m     23\u001B[0m         \u001B[38;5;28;01mreturn\u001B[39;00m {\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mmessages\u001B[39m\u001B[38;5;124m\"\u001B[39m: outputs}\n\u001B[1;32m     26\u001B[0m tool_node \u001B[38;5;241m=\u001B[39m BasicToolNode(tools\u001B[38;5;241m=\u001B[39mtools)\n\u001B[0;32m---> 27\u001B[0m graph_builder\u001B[38;5;241m.\u001B[39madd_node(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mtools\u001B[39m\u001B[38;5;124m\"\u001B[39m, tool_node)\n",
      "File \u001B[0;32m/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langgraph/graph/state.py:331\u001B[0m, in \u001B[0;36mStateGraph.add_node\u001B[0;34m(self, node, action, metadata, input, retry)\u001B[0m\n\u001B[1;32m    329\u001B[0m     \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mRuntimeError\u001B[39;00m\n\u001B[1;32m    330\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m node \u001B[38;5;129;01min\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mnodes:\n\u001B[0;32m--> 331\u001B[0m     \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mNode `\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mnode\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m` already present.\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m    332\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m node \u001B[38;5;241m==\u001B[39m END \u001B[38;5;129;01mor\u001B[39;00m node \u001B[38;5;241m==\u001B[39m START:\n\u001B[1;32m    333\u001B[0m     \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mNode `\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mnode\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m` is reserved.\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n",
      "\u001B[0;31mValueError\u001B[0m: Node `tools` already present."
     ]
    }
   ],
   "source": [
    "import json\n",
    "from langchain_core.messages import ToolMessage\n",
    "\n",
    "\n",
    "class BasicToolNode:\n",
    "    \"\"\"運行最后一個AIMessage中請求的工具\"\"\"\n",
    "\n",
    "    def __init__(self, tools: list) -> None:\n",
    "        self.tools_by_name = {tool.name: tool for tool in tools}\n",
    "\n",
    "    def __call__(self, inputs: dict):\n",
    "        if messages := inputs.get(\"messages\", []):\n",
    "            message = messages[-1]\n",
    "        else:\n",
    "            raise ValueError(\"No message found in input\")\n",
    "        outputs = []\n",
    "        for tool_call in message.tool_calls:\n",
    "            print(tool_call[\"name\"])\n",
    "            print(self.tools_by_name[tool_call[\"name\"]])\n",
    "            tool_result = self.tools_by_name[tool_call[\"name\"]].invoke(tool_call[\"args\"])\n",
    "            outputs.append(\n",
    "                ToolMessage(content=json.dumps(tool_result), name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"], ))\n",
    "        return {\"messages\": outputs}\n",
    "\n",
    "\n",
    "tool_node = BasicToolNode(tools=tools)\n",
    "graph_builder.add_node(\"tools\", tool_node)\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T03:16:10.081461Z",
     "start_time": "2024-11-29T03:16:10.051893Z"
    }
   },
   "id": "4ece1942d12628dc",
   "execution_count": 69
  },
  {
   "cell_type": "markdown",
   "source": [
    "定义边"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "23bce859d459f96a"
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Adding an edge to a graph that has already been compiled. This will not be reflected in the compiled graph.\n"
     ]
    },
    {
     "ename": "ValueError",
     "evalue": "Branch with name `route_tools` already exists for node `chatbot`",
     "output_type": "error",
     "traceback": [
      "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
      "\u001B[0;31mValueError\u001B[0m                                Traceback (most recent call last)",
      "Cell \u001B[0;32mIn[65], line 17\u001B[0m\n\u001B[1;32m     13\u001B[0m         \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mtools\u001B[39m\u001B[38;5;124m\"\u001B[39m\n\u001B[1;32m     14\u001B[0m     \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124m__end__\u001B[39m\u001B[38;5;124m\"\u001B[39m  \u001B[38;5;66;03m# ' tools_condition '函數返回\"tools\"，表示LLM要求使用工具，返回\"__end__\"直接結束。\u001B[39;00m\n\u001B[0;32m---> 17\u001B[0m graph_builder\u001B[38;5;241m.\u001B[39madd_conditional_edges(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mchatbot\u001B[39m\u001B[38;5;124m\"\u001B[39m, route_tools,\n\u001B[1;32m     18\u001B[0m                                     \u001B[38;5;66;03m# The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node# It defaults to the identity function, but if you# want to use a node named something else apart from \"tools\",# You can update the value of the dictionary to something else# e.g., \"tools\": \"my_tools\"{\"tools\": \"tools\", \"__end__\": \"__end__\"},\u001B[39;00m\n\u001B[1;32m     19\u001B[0m                                     )\n\u001B[1;32m     20\u001B[0m \u001B[38;5;66;03m# 任何時候調用一個工具，我們都會流轉到聊天機器人\u001B[39;00m\n\u001B[1;32m     21\u001B[0m graph_builder\u001B[38;5;241m.\u001B[39madd_edge(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mtools\u001B[39m\u001B[38;5;124m\"\u001B[39m, \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mchatbot\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n",
      "File \u001B[0;32m/opt/anaconda3/envs/ai_312/lib/python3.12/site-packages/langgraph/graph/graph.py:293\u001B[0m, in \u001B[0;36mGraph.add_conditional_edges\u001B[0;34m(self, source, path, path_map, then)\u001B[0m\n\u001B[1;32m    291\u001B[0m \u001B[38;5;66;03m# validate the condition\u001B[39;00m\n\u001B[1;32m    292\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m name \u001B[38;5;129;01min\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mbranches[source]:\n\u001B[0;32m--> 293\u001B[0m     \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\n\u001B[1;32m    294\u001B[0m         \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mBranch with name `\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mpath\u001B[38;5;241m.\u001B[39mname\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m` already exists for node \u001B[39m\u001B[38;5;124m\"\u001B[39m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124m`\u001B[39m\u001B[38;5;132;01m{\u001B[39;00msource\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m`\u001B[39m\u001B[38;5;124m\"\u001B[39m\n\u001B[1;32m    295\u001B[0m     )\n\u001B[1;32m    296\u001B[0m \u001B[38;5;66;03m# save it\u001B[39;00m\n\u001B[1;32m    297\u001B[0m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mbranches[source][name] \u001B[38;5;241m=\u001B[39m Branch(path, path_map_, then)\n",
      "\u001B[0;31mValueError\u001B[0m: Branch with name `route_tools` already exists for node `chatbot`"
     ]
    }
   ],
   "source": [
    "from typing import Literal\n",
    "\n",
    "\n",
    "def route_tools(state: State, ) -> Literal[\"tools\", \"__end__\"]:\n",
    "    \"\"\"如果最后一條消息，在conditional_edge中使用路由到ToolNode,就調用工具。否則，路線到終點。\"\"\"\n",
    "    if isinstance(state, list):\n",
    "        ai_message = state[-1]\n",
    "    elif messages := state.get(\"messages\", []):\n",
    "        ai_message = messages[-1]\n",
    "    else:\n",
    "        raise ValueError(f\"在tool_edge的輸入狀態中沒有找到消息: {state}\")\n",
    "    if hasattr(ai_message, \"tool_calls\") and len(ai_message.tool_calls) > 0:\n",
    "        return \"tools\"\n",
    "    return \"__end__\"  # ' tools_condition '函數返回\"tools\"，表示LLM要求使用工具，返回\"__end__\"直接結束。\n",
    "\n",
    "\n",
    "graph_builder.add_conditional_edges(\"chatbot\", route_tools,\n",
    "                                    # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node# It defaults to the identity function, but if you# want to use a node named something else apart from \"tools\",# You can update the value of the dictionary to something else# e.g., \"tools\": \"my_tools\"{\"tools\": \"tools\", \"__end__\": \"__end__\"},\n",
    "                                    )\n",
    "# 任何時候調用一個工具，我們都會流轉到聊天機器人\n",
    "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
    "graph_builder.set_entry_point(\"chatbot\")\n",
    "graph = graph_builder.compile()"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T03:15:39.698128Z",
     "start_time": "2024-11-29T03:15:39.669846Z"
    }
   },
   "id": "46e5ece2879e95af",
   "execution_count": 65
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": "<IPython.core.display.Image object>"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "try:\n",
    "    display(Image(graph.get_graph().draw_mermaid_png()))\n",
    "except:  # This requires some extra dependencies and is optional\n",
    "    pass\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T03:09:37.714523Z",
     "start_time": "2024-11-29T03:09:36.851526Z"
    }
   },
   "id": "2b04c714accdcf33",
   "execution_count": 55
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Assistant: 为了提供最近一周使用最多的前五个标签，我需要先了解数据库中存储这些信息的表结构。这通常涉及到包含标签和其使用时间戳的表。首先，我将获取数据库中的表列表，然后确定哪个表包含了我们需要的信息。让我们开始第一步，列出所有可用的表。\n",
      "\n",
      "现在，我将调用 `sql_db_list_tables` 函数来获取数据库中所有的表名。\n",
      "sql_db_list_tables\n",
      "db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x10e688f50>\n",
      "Assistant: \"Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\"\n",
      "Assistant: 我们现在知道了数据库中有哪些表，但还不清楚哪个表包含了标签信息。通常，标签可能会与`Track`表相关联，也可能存在于其他表中，比如`Genre`，如果这里的标签指的是音乐流派的话。我们需要进一步调查这些表的结构来确定哪个表存储了标签数据以及使用时间戳。\n",
      "\n",
      "接下来，我将调用 `sql_db_schema` 函数来获取可能包含标签信息的表模式，例如`Track`和`Genre`表。这将帮助我们理解哪些字段可能对应于标签和它们的使用情况。\n",
      "sql_db_schema\n",
      "description='Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3' db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x10e688f50>\n",
      "Assistant: \"\\nCREATE TABLE \\\"Genre\\\" (\\n\\t\\\"GenreId\\\" INTEGER NOT NULL, \\n\\t\\\"Name\\\" NVARCHAR(120), \\n\\tPRIMARY KEY (\\\"GenreId\\\")\\n)\\n\\n/*\\n3 rows from Genre table:\\nGenreId\\tName\\n1\\tRock\\n2\\tJazz\\n3\\tMetal\\n*/\\n\\n\\nCREATE TABLE \\\"Track\\\" (\\n\\t\\\"TrackId\\\" INTEGER NOT NULL, \\n\\t\\\"Name\\\" NVARCHAR(200) NOT NULL, \\n\\t\\\"AlbumId\\\" INTEGER, \\n\\t\\\"MediaTypeId\\\" INTEGER NOT NULL, \\n\\t\\\"GenreId\\\" INTEGER, \\n\\t\\\"Composer\\\" NVARCHAR(220), \\n\\t\\\"Milliseconds\\\" INTEGER NOT NULL, \\n\\t\\\"Bytes\\\" INTEGER, \\n\\t\\\"UnitPrice\\\" NUMERIC(10, 2) NOT NULL, \\n\\tPRIMARY KEY (\\\"TrackId\\\"), \\n\\tFOREIGN KEY(\\\"MediaTypeId\\\") REFERENCES \\\"MediaType\\\" (\\\"MediaTypeId\\\"), \\n\\tFOREIGN KEY(\\\"GenreId\\\") REFERENCES \\\"Genre\\\" (\\\"GenreId\\\"), \\n\\tFOREIGN KEY(\\\"AlbumId\\\") REFERENCES \\\"Album\\\" (\\\"AlbumId\\\")\\n)\\n\\n/*\\n3 rows from Track table:\\nTrackId\\tName\\tAlbumId\\tMediaTypeId\\tGenreId\\tComposer\\tMilliseconds\\tBytes\\tUnitPrice\\n1\\tFor Those About To Rock (We Salute You)\\t1\\t1\\t1\\tAngus Young, Malcolm Young, Brian Johnson\\t343719\\t11170334\\t0.99\\n2\\tBalls to the Wall\\t2\\t2\\t1\\tU. Dirkschneider, W. Hoffmann, H. Frank, P. Baltes, S. Kaufmann, G. Hoffmann\\t342562\\t5510424\\t0.99\\n3\\tFast As a Shark\\t3\\t2\\t1\\tF. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman\\t230619\\t3990994\\t0.99\\n*/\"\n",
      "Assistant: 从`Track`和`Genre`表的模式中，我们可以看到`Genre`表包含了不同流派（可以视为标签）的信息，而`Track`表有一个`GenreId`字段来引用这些流派。但是，我们没有直接找到关于标签使用时间戳的信息。\n",
      "\n",
      "为了找出最近一周内使用的前五个标签，我们需要知道哪个表或哪些表记录了包含时间戳的播放或购买历史。这可能涉及到了`Invoice`, `InvoiceLine`, 或者其他类似的表，它们通常会记录销售日期，如果标签的“使用”是指用户购买了某个音乐作品的话。\n",
      "\n",
      "让我们先检查一下`Invoice`和`InvoiceLine`表的结构，以确定是否可以从这些表中提取出所需的时间信息。\n",
      "sql_db_schema\n",
      "description='Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3' db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x10e688f50>\n",
      "Assistant: \"\\nCREATE TABLE \\\"Invoice\\\" (\\n\\t\\\"InvoiceId\\\" INTEGER NOT NULL, \\n\\t\\\"CustomerId\\\" INTEGER NOT NULL, \\n\\t\\\"InvoiceDate\\\" DATETIME NOT NULL, \\n\\t\\\"BillingAddress\\\" NVARCHAR(70), \\n\\t\\\"BillingCity\\\" NVARCHAR(40), \\n\\t\\\"BillingState\\\" NVARCHAR(40), \\n\\t\\\"BillingCountry\\\" NVARCHAR(40), \\n\\t\\\"BillingPostalCode\\\" NVARCHAR(10), \\n\\t\\\"Total\\\" NUMERIC(10, 2) NOT NULL, \\n\\tPRIMARY KEY (\\\"InvoiceId\\\"), \\n\\tFOREIGN KEY(\\\"CustomerId\\\") REFERENCES \\\"Customer\\\" (\\\"CustomerId\\\")\\n)\\n\\n/*\\n3 rows from Invoice table:\\nInvoiceId\\tCustomerId\\tInvoiceDate\\tBillingAddress\\tBillingCity\\tBillingState\\tBillingCountry\\tBillingPostalCode\\tTotal\\n1\\t2\\t2021-01-01 00:00:00\\tTheodor-Heuss-Stra\\u00dfe 34\\tStuttgart\\tNone\\tGermany\\t70174\\t1.98\\n2\\t4\\t2021-01-02 00:00:00\\tUllev\\u00e5lsveien 14\\tOslo\\tNone\\tNorway\\t0171\\t3.96\\n3\\t8\\t2021-01-03 00:00:00\\tGr\\u00e9trystraat 63\\tBrussels\\tNone\\tBelgium\\t1000\\t5.94\\n*/\\n\\n\\nCREATE TABLE \\\"InvoiceLine\\\" (\\n\\t\\\"InvoiceLineId\\\" INTEGER NOT NULL, \\n\\t\\\"InvoiceId\\\" INTEGER NOT NULL, \\n\\t\\\"TrackId\\\" INTEGER NOT NULL, \\n\\t\\\"UnitPrice\\\" NUMERIC(10, 2) NOT NULL, \\n\\t\\\"Quantity\\\" INTEGER NOT NULL, \\n\\tPRIMARY KEY (\\\"InvoiceLineId\\\"), \\n\\tFOREIGN KEY(\\\"TrackId\\\") REFERENCES \\\"Track\\\" (\\\"TrackId\\\"), \\n\\tFOREIGN KEY(\\\"InvoiceId\\\") REFERENCES \\\"Invoice\\\" (\\\"InvoiceId\\\")\\n)\\n\\n/*\\n3 rows from InvoiceLine table:\\nInvoiceLineId\\tInvoiceId\\tTrackId\\tUnitPrice\\tQuantity\\n1\\t1\\t2\\t0.99\\t1\\n2\\t1\\t4\\t0.99\\t1\\n3\\t2\\t6\\t0.99\\t1\\n*/\"\n",
      "Assistant: 从`Invoice`和`InvoiceLine`表的模式中，我们发现`Invoice`表有一个`InvoiceDate`字段，它记录了发票（购买）日期。`InvoiceLine`表通过`TrackId`关联到`Track`表，并且每条记录表示一个被购买的音轨。因此，我们可以将`Invoice`, `InvoiceLine`以及`Track`表连接起来，以获取每个标签在最近一周内的使用次数。\n",
      "\n",
      "现在我将编写一个SQL查询来找出最近一周内使用最多的前五个标签。这个查询会考虑`InvoiceDate`在过去7天内，并按标签使用次数降序排序，然后选择前五名。我们需要确保查询正确无误，所以我将先用`sql_db_query_checker`来检查一下这个查询。\n",
      "sql_db_query_checker\n",
      "description='Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query!' db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x10e688f50> llm=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x1112d86b0>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x1112d9e80>, root_client=<openai.OpenAI object at 0x110ac5e50>, root_async_client=<openai.AsyncOpenAI object at 0x11092bf20>, model_name='qwen-max', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'), openai_api_base='https://dashscope.aliyuncs.com/compatible-mode/v1') llm_chain=LLMChain(verbose=False, prompt=PromptTemplate(input_variables=['dialect', 'query'], input_types={}, partial_variables={}, template='\\n{query}\\nDouble check the {dialect} query above for common mistakes, including:\\n- Using NOT IN with NULL values\\n- Using UNION when UNION ALL should have been used\\n- Using BETWEEN for exclusive ranges\\n- Data type mismatch in predicates\\n- Properly quoting identifiers\\n- Using the correct number of arguments for functions\\n- Casting to the correct data type\\n- Using the proper columns for joins\\n\\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\\n\\nOutput the final SQL query only.\\n\\nSQL Query: '), llm=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x1112d86b0>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x1112d9e80>, root_client=<openai.OpenAI object at 0x110ac5e50>, root_async_client=<openai.AsyncOpenAI object at 0x11092bf20>, model_name='qwen-max', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'), openai_api_base='https://dashscope.aliyuncs.com/compatible-mode/v1'), output_parser=StrOutputParser(), llm_kwargs={})\n",
      "Assistant: \"```sql\\nSELECT g.Name AS Genre, COUNT(il.InvoiceLineId) AS Purchases \\nFROM Invoice i \\nJOIN InvoiceLine il ON i.InvoiceId = il.InvoiceId \\nJOIN Track t ON il.TrackId = t.TrackId \\nJOIN Genre g ON t.GenreId = g.GenreId \\nWHERE i.InvoiceDate >= DATE('now', '-7 days') \\nGROUP BY g.Name \\nORDER BY Purchases DESC \\nLIMIT 5;\\n```\"\n",
      "Assistant: 查询检查没有报告任何问题，这意味着我们的SQL语句在语法上是正确的。现在我将执行这个查询以获取最近一周内使用最多的前五个标签。\n",
      "\n",
      "我将调用 `sql_db_query` 函数来执行以下查询：\n",
      "\n",
      "```sql\n",
      "SELECT g.Name AS Genre, COUNT(il.InvoiceLineId) AS Purchases\n",
      "FROM Invoice i\n",
      "JOIN InvoiceLine il ON i.InvoiceId = il.InvoiceId\n",
      "JOIN Track t ON il.TrackId = t.TrackId\n",
      "JOIN Genre g ON t.GenreId = g.GenreId\n",
      "WHERE i.InvoiceDate >= DATE('now', '-7 days')\n",
      "GROUP BY g.Name\n",
      "ORDER BY Purchases DESC\n",
      "LIMIT 5;\n",
      "```\n",
      "\n",
      "这将返回我们所需的标签及其购买次数。让我们执行这个查询。\n",
      "sql_db_query\n",
      "description=\"Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.\" db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x10e688f50>\n",
      "Assistant: \"[('Rock', 182), ('Latin', 103), ('Metal', 64), ('Alternative & Punk', 62), ('Jazz', 22)]\"\n",
      "Assistant: 根据查询结果，在最近一周内使用最多的前五个标签（流派）及其购买次数如下：\n",
      "\n",
      "1. Rock - 182次\n",
      "2. Latin - 103次\n",
      "3. Metal - 64次\n",
      "4. Alternative & Punk - 62次\n",
      "5. Jazz - 22次\n",
      "\n",
      "这些数据表明Rock是最受欢迎的标签，其次是Latin和Metal。如果您需要进一步的信息或者不同时间段的数据，请告诉我。\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.messages import BaseMessage\n",
    "# while True:\n",
    "#     user_input = input(\"User: \")\n",
    "#     if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n",
    "#         print(\"Goodbye!\")\n",
    "#         break\n",
    "#     for event in graph.stream({\"messages\": [(\"user\", user_input)]}):\n",
    "#         for value in event.values():\n",
    "#             if isinstance(value[\"messages\"][-1], BaseMessage):\n",
    "#                 print(\"Assistant:\", value[\"messages\"][-1].content)\n",
    "\n",
    "for event in graph.stream({\"messages\": [(\"user\", \"最近一周使用标签前五\")]}):\n",
    "        for value in event.values():\n",
    "            if isinstance(value[\"messages\"][-1], BaseMessage):\n",
    "                print(\"Assistant:\", value[\"messages\"][-1].content)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T03:11:30.878059Z",
     "start_time": "2024-11-29T03:10:36.319218Z"
    }
   },
   "id": "3743209700def886",
   "execution_count": 56
  },
  {
   "cell_type": "markdown",
   "source": [
    "# 自定义sqlCheck\n",
    "问题->提示语->LLM生成SQL语句->sql 语句check->执行sql->返回结果"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "5d17a900dd9a7bf1"
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "from langchain_community.tools import QuerySQLCheckerTool\n",
    "\n",
    "# sql_check = QuerySQLCheckerTool(db=db, llm=llm, description=query_sql_checker_tool_description)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:19:11.273572Z",
     "start_time": "2024-11-29T01:19:11.270482Z"
    }
   },
   "id": "59d8afb8331840ae",
   "execution_count": 17
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "\n",
    "from dotenv import load_dotenv\n",
    "import os\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "load_dotenv()\n",
    "prompt = ChatPromptTemplate.from_template(\"\"\"\n",
    "    你是个语言组织专家.\n",
    "    根据用户输入，重新组织为完整语言描述。\n",
    "    {input}\n",
    "    语言组织方式限制:\n",
    "    - 仅根据用户输入合理组织语言为一个完整的句子，切勿发散思考\n",
    "    - 可简单添加一些简单的描述性语言，但不要过多\n",
    "\"\"\")\n",
    "llm = ChatOpenAI(\n",
    "    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=\"sk-xxx\",\n",
    "    openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "    model_name=\"qwen-plus\",\n",
    "    max_retries=0,\n",
    ")\n",
    "chain = prompt | llm"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T01:39:06.624770Z",
     "start_time": "2024-11-29T01:39:06.506934Z"
    }
   },
   "id": "ff37dcbb7f1f133",
   "execution_count": 45
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "data": {
      "text/plain": "AIMessage(content='用户请求的任务是设置一个定时发送的指令，业务类型为1，涉及的业务系统名称为通用业务主题，未指定分组名称和用户标签名称。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 36, 'prompt_tokens': 106, 'total_tokens': 142, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen-plus', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-3fe251cf-2b9f-42cb-956d-c789d9790f1b-0', usage_metadata={'input_tokens': 106, 'output_tokens': 36, 'total_tokens': 142, 'input_token_details': {}, 'output_token_details': {}})"
     },
     "execution_count": 75,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chain.invoke({\"input\": \"任务类型：定时发送；业务类型：1；业务系统名称：通用业务主题；分组名称：-；用户标签名称：-\"},{\"recursion_limit\": 3})"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-11-29T05:36:39.208711Z",
     "start_time": "2024-11-29T05:36:36.736126Z"
    }
   },
   "id": "1e42a580e4cc2ea",
   "execution_count": 75
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
